﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using CI.Interfaces;
using CI.Utils;
using Microsoft.Dss.Core;
using Microsoft.Dss.Hosting;
using System.Linq;

namespace CI.Mesopotamia
{
    ///<summary>
    /// Boot straps the hosting scenario from either started in the Cmd app or WPF app.
    ///</summary>
    public abstract class BootStrap
    {
        public string ContractName;
        public string ManifestName;
        public string ScenarioName;
        protected abstract LaunchType StartupMode();
        protected abstract void OnStartup();
        protected abstract void OnManifestStarted();
        protected abstract void RoboticsEngineStarting();
        protected abstract void ReportError(string message, string caption);
        protected abstract void PreScenarioHostBootstrap();
        protected abstract void RestartPreCleanUp();
        private Semaphore _semaphore;
        private DssRuntimeConfiguration _dssConfig;
        private string[] _manifestFiles;
        private SimEnv _simEnv = SimEnv.Instance;
        public ManualResetEvent StartTrigger = new ManualResetEvent(false);
        private IScenario _scenario;
        protected bool WasRestart;
        private int _basePort = 50000;
        public event Action<String, String, String, bool> ScenarioLoaded;
        public List<Guid> SelectedOrganismsToLoad = new List<Guid>();
        public bool UsingHW;

        public BootstrapResult Start(TimeSpan breakTime, bool wasRestart)
        {
            // reset values
            Globals.BreakTime = breakTime;

            WasRestart = wasRestart;

            if (Globals.IsAlreadyRunning())
                return new BootstrapResult(false, false, string.Empty, string.Empty, string.Empty);

            _simEnv.UsingHW = UsingHW;

            // use a semaphore to sync between 
            _semaphore = new Semaphore(0, 1, "MSRB");

            _simEnv.TraceLogger.LogDebug("PreBootStrapSetup");
            PreBootStrapSetup();

            var roboticsPath = GetRoboticsPath();
            if (string.IsNullOrEmpty(roboticsPath))
                return new BootstrapResult(false, false, string.Empty, string.Empty, string.Empty);

            _simEnv.TraceLogger.LogDebug("Loading Scenario files");
            _scenario = LoadScenario();

            var port = Environment.GetCommandLineArgs().FirstOrDefault(commandLineArg => commandLineArg.Contains("/port:"));
            if (port != null)
            {
                _basePort = int.Parse(port.Substring(port.IndexOf(":") + 1));
            }

            _dssConfig = RoboticsConfig.LoadDssConfig(_basePort);
            _dssConfig.SecuritySettings = Path.Combine(roboticsPath, "SecuirtySettings.xml");

            _manifestFiles = GenerateManifest(roboticsPath);

            CreateAndStartRobotics();

            if (_simEnv.BreakExit)
                return new BootstrapResult(false, false, string.Empty, string.Empty, string.Empty);

            _simEnv.ClearServiceFailureRestartFlag();

            do
            {
                PreScenarioHostBootstrap();

                if (_simEnv.UsingHW)
                    UpdateOrganismsRequiredForHw(_scenario);

                var bootStrap = new ScenarioHostBootstrap();
                bootStrap.Start(_simEnv.HostEnvPhysical, _scenario, SelectedOrganismsToLoad);

                CheckForRestartAndCleanUp();

            } while (Globals.ContinousRestart && !_simEnv.ServiceFailureRestart);

            if (_simEnv != null && _simEnv.HostEnvPhysical != null)
                _simEnv.HostEnvPhysical.ShutDownService();

            StopRoboticsEngine();

            return new BootstrapResult(_simEnv.ServiceFailureRestart, _simEnv.UsingHW, ScenarioName, ContractName, ManifestName);
        }

        private static void UpdateOrganismsRequiredForHw(IScenario scenario)
        {
            // if hd setting, set fitness function to nothing
            scenario.MaxLoadCount = 1;
            // trim size
            scenario.FitnessFunction = Globals.NoFitnessFunction;
            scenario.RequiredOrganisms.RemoveRange(scenario.MaxLoadCount,
                                                   scenario.RequiredOrganisms.Count - scenario.MaxLoadCount);
            scenario.RequiredOrganisms[0].Locations.RemoveRange(scenario.MaxLoadCount,
                                                                scenario.RequiredOrganisms[0].Locations.Count -
                                                                scenario.MaxLoadCount);
        }

        private void CheckForRestartAndCleanUp()
        {
            if (Globals.ContinousRestart)
            {
                // clear break flag
                _simEnv.ClearBreakFlag();

                RestartPreCleanUp();

                // reload the scenario
                _scenario = LoadScenario();
            }

            // if restart
            _simEnv.TraceLogger.LogDebug("Restart Simulation :  " + Globals.ContinousRestart);
        }

        private void PreBootStrapSetup()
        {
            _simEnv.TraceLogger.LogDebug("Copying Scenario Config files");

            // copy scenario configs
            RoboticsConfig.CopyScenarioFiles();

            OnStartup();

            Globals.LaunchedFrom = StartupMode();

            StartTrigger.WaitOne();

            CheckScreenSaverSettings();

            // startup
            _simEnv.TraceLogger.LogDebug("Startup Mesopotamia " + Globals.LaunchedFrom);

            // set the thread name(helpfull when debugging, note the debugger if running will set a name)
            if (Thread.CurrentThread.Name == null)
                Thread.CurrentThread.Name = "Mesopotaima Main Thread";
        }

        public void StopRoboticsEngine()
        {
            if (_simEnv.ServiceFailureRestart)
                return;

            _simEnv.TraceLogger.LogWarn("Shuting down Robotics Engine - Start :" + DateTime.Now.ToLocalTime());

            try
            {
                // shutdown
                // Wait here until DSS Environment has been shut down
                DssEnvironment.Shutdown();

                // make sure it has shutdown except if a software/hardware failure as this will block indefinatley
                DssEnvironment.WaitForShutdown();

            }
            catch (Exception exception)
            {
                _simEnv.TraceLogger.LogException("Exception on shutting down", exception);
            }

            _simEnv.TraceLogger.LogWarn("Shuting down Robotics Engine - Finish :" + DateTime.Now.ToLocalTime());

        }

        private void CreateAndStartRobotics()
        {
            RoboticsEngineStarting();

            _simEnv.TraceLogger.LogDebug("CreateAndStartRobotics");
            foreach (var file in _manifestFiles)
            {
                _simEnv.TraceLogger.LogDebug("Using manifest file : " + file);
            }

            StartRoboticsEngine(_dssConfig, _manifestFiles);

            OnManifestStarted();
        }

        private static void CheckScreenSaverSettings()
        {
            // set the screen saver
            // todo : add method to detect and prompt user if not set, also not to nag them each time

            if (!ScreenSaver.AreWeCurrentScreenSaver())
            {
                ScreenSaver.SetScreenSaver();
            }
        }

        private string GetRoboticsPath()
        {
            string roboticsPath = RoboticsConfig.FindRoboticsFolders();

            _simEnv.TraceLogger.LogDebug("Robotics Path : " + roboticsPath);

            if (roboticsPath == null)
            {
                const string error = "Unable to find robotics folder";
                _simEnv.TraceLogger.LogWarn(error);
                ReportError(error, "Error");
                return null;
            }
            return roboticsPath;
        }

        private void StartRoboticsEngine(DssRuntimeConfiguration dssConfig, string[] manifestFiles)
        {
            try
            {
                _simEnv.TraceLogger.LogDebug("Initializing DSS");

                // Initialize DSS without manifests
                DssEnvironment.Initialize(dssConfig, manifestFiles);
            }
            catch (InvalidOperationException ex)
            {
                var startError = string.Format("Error Starting Microsoft Robotics Engine on port {0}",
                                                  dssConfig.PublicHttpPort);
                _simEnv.TraceLogger.LogException("Microsoft Robotics Engine", ex);
                ReportError(startError + Environment.NewLine + ex.Message, "Microsoft Robotics Engine");
                _simEnv.TriggerServiceAndScenarioRestart(startError);
                StopRoboticsEngine();

                if (ex.Message.Contains("TCP"))
                {
                    throw new PortException();
                }

                throw;
            }

            CreateRoboticsService();
        }

        private void CreateRoboticsService()
        {
            var resultPort = DssEnvironment.CreateService(ContractName);

            _simEnv.TraceLogger.LogDebug("Service Creation : " + ContractName);
            _simEnv.TraceLogger.LogDebug("Service Creation Result : " + resultPort);
            _simEnv.TraceLogger.LogDebug("Waiting on semaphore sync - Start");

            // use a mutex to sync between 
            if (!_semaphore.WaitOne(Globals.StartupRoboticsTimeout))
            {
                const string error =
                    "Unable to start robotics engine, please exit and try starting the application again";
                _simEnv.TraceLogger.LogWarn(error);
                ReportError(error, "Error");
                throw new Exception("Unable to create service");
            }
        }

        private IScenario LoadScenario()
        {
            var sf = new ScenarioFactory();
            IScenario loadScenario = sf.GetScenario();

            // if the scenario has not been set via the wizard then load the preffered scenario
            if (String.IsNullOrEmpty(ScenarioName))
                ScenarioName = sf.GetPreferredScenarioName();

            loadScenario.ScenarioName = ScenarioName;
            loadScenario.Load();

            if (ScenarioLoaded != null)
                ScenarioLoaded(ScenarioName, ContractName, ManifestName, _simEnv.UsingHW);

            if (String.IsNullOrEmpty(ManifestName))
            {
                ManifestName = loadScenario.ManifestFile;
            }

            return loadScenario;
        }

        private string[] GenerateManifest(string roboticsPath)
        {
            _simEnv.TraceLogger.LogDebug("GenerateManifest");
            string manifestFilePath = Path.Combine(roboticsPath, ManifestName);

            // make sure the manifest exists
            if (!File.Exists(manifestFilePath))
            {
                string error = "ERROR: Manifest file does not exist : " + manifestFilePath;
                _simEnv.TraceLogger.LogWarn(error);
                ReportError(error, "Error");
                program.Exit(0);
            }

            // generate the manifest files
            return new[] { manifestFilePath };
        }
    }

    public class PortException : Exception
    { }

    public class BootstrapResult
    {
        public BootstrapResult(bool failureRestart, bool usingHW, string scenarioName, string contractName, string manifestName)
        {
            FailureRestart = failureRestart;
            UsingHW = usingHW;
            ScenarioName = scenarioName;
            ContractName = contractName;
            ManifestName = manifestName;
        }


        public bool FailureRestart { get; private set; }
        public bool UsingHW { get; private set; }
        public string ScenarioName { get; private set; }
        public string ContractName { get; private set; }
        public string ManifestName { get; private set; }

        public override string ToString()
        {
            string result = null;

            result += "Failure Result :" + FailureRestart + Environment.NewLine;
            result += "ScenarioName :" + ScenarioName + Environment.NewLine;
            result += "ContractName :" + ContractName + Environment.NewLine;
            result += "ManifestName :" + ManifestName + Environment.NewLine;
            result += "UsingHW :" + UsingHW + Environment.NewLine;

            return result;
        }
    }
}